Database Locks
contents
1. 데이터베이스 락(Lock)이란?
락(Lock) 은 데이터베이스 엔진이 여러 사용자가 동시에 같은 데이터에 접근할 때 데이터의 일관성을 유지하기 위해 사용하는 동기화 메커니즘입니다.
- 문제점: 만약 두 명의 사용자가 정확히 같은 밀리초(millisecond)에 같은 행(Row)을 수정하려고 하면, 데이터가 깨지거나 덮어쓰여질 수 있습니다 (경합 조건, Race Condition).
- 해결책: 데이터베이스는 사용자 A를 위해 해당 데이터를 "잠급니다(Lock)". 사용자 B는 사용자 A가 작업을 마칠 때까지 대기열에서 기다려야 합니다.
비유: 비행기 화장실을 생각해보세요.
- 한 번에 한 사람만 들어갈 수 있습니다.
- 문 밖의 "사용 중(Occupied)" 불빛이 바로 락(Lock) 입니다.
- 밖에서 기다리는 사람들의 줄이 블로킹 큐(Blocking Queue) 입니다.
2. 락의 두 가지 주요 유형 (Lock Modes)
"읽기(Reading)"와 "쓰기(Writing)"의 차이를 이해하는 것이 핵심입니다.
A. 공유 잠금 (Shared Lock / S-Lock) – "독자를 위한 잠금"
- 목적: 데이터를 읽을 때(
SELECT) 사용합니다. - 동작:
- "나 지금 이거 읽고 있어. 내가 보는 동안 내용 바꾸지 마."
- 호환성: 여러 사용자가 동시에 같은 행에 대해 S-Lock을 가질 수 있습니다. (여러 사람이 같은 책을 동시에 읽는 것은 가능함).
- 비호환성: 누군가 읽고 있다면, 다른 사람은 배타적(쓰기) 잠금을 얻을 수 없습니다.
B. 배타적 잠금 (Exclusive Lock / X-Lock) – "저자를 위한 잠금"
- 목적: 데이터를 수정할 때(
INSERT,UPDATE,DELETE) 사용합니다. - 동작:
- "나 지금 이거 고치고 있어. 아무도 보지도 말고 건드리지도 마."
- 비호환성: 사용자 A가 X-Lock을 가지고 있다면, 사용자 B는 어떠한 락(읽기든 쓰기든)도 얻을 수 없습니다. 사용자 B는 멈춰서 기다려야(Blocked) 합니다.
호환성 매트릭스 (The Compatibility Matrix):
| 현재 걸려있는 락 | 사용자 B가 읽기(S) 요청 | 사용자 B가 쓰기(X) 요청 |
|---|---|---|
| 없음 (None) | ✅ 허용됨 | ✅ 허용됨 |
| 읽기 (S) | ✅ 허용됨 | ❌ 대기 (WAIT) |
| 쓰기 (X) | ❌ 대기 (WAIT) | ❌ 대기 (WAIT) |
3. 잠금 단위 (Lock Granularity)
데이터베이스가 한 번에 얼마나 많은 데이터를 잠글까요?
- 행(Row) 수준 잠금:
- 특정 행 하나만 잠급니다 (예: User ID 5).
- 장점: 동시성이 높음 (다른 사람들은 User ID 6을 건드릴 수 있음).
- 단점: 메모리 오버헤드가 큼 (수백만 개의 작은 자물쇠를 관리하는 것은 비쌈).
- 페이지(Page) 수준 잠금:
- 저장소의 "페이지"(예: User ID 1~50이 들어있는 8KB 블록) 단위를 잠급니다.
- 중간 단계입니다.
- 테이블(Table) 수준 잠금:
Users테이블 전체를 통째로 잠급니다.- 장점: 오버헤드가 매우 적음 (관리할 자물쇠가 딱 하나).
- 단점: 동시성 제로. 모든 사람이 기다려야 함. 주로
ALTER TABLE(스키마 변경) 작업 시 사용됩니다.
락 에스컬레이션 (Lock Escalation): 만약 트랜잭션이 너무 많은 행 락(예: 10만 개 행 업데이트)을 얻으려고 하면, DB는 메모리를 아끼기 위해 자동으로 테이블 락으로 "승격(Escalation)"시켜버립니다. 이 순간 다른 모든 사용자가 갑자기 차단될 수 있습니다.
4. 비관적 락 vs. 낙관적 락 (Pessimistic vs. Optimistic)
애플리케이션 개발자에게 가장 중요한 개념입니다.
A. 비관적 락 (Pessimistic Locking - "아무도 믿지 마라")
충돌이 반드시 일어날 것이라고 가정하고, 데이터를 건드리기 전에 미리 잠가버립니다.
- SQL:
SELECT * FROM Products WHERE id=1 FOR UPDATE; - 동작: 데이터베이스는 즉시 X-Lock을 겁니다. 트랜잭션이 끝날 때까지 아무도 이 행을 읽거나 쓸 수 없습니다.
- 사용 사례: 경쟁이 치열한 데이터 (예: 콘서트의 마지막 남은 티켓 판매).
B. 낙관적 락 (Optimistic Locking - "잘 될 거야")
충돌이 드물 것이라고 가정합니다. DB 행을 잠그지 않습니다. 대신, 저장할 때 변경 여부를 확인합니다.
- 메커니즘:
version(숫자) 컬럼을 추가합니다. - 로직:
- 행을 읽습니다 (버전은 1).
- 사용자가 화면에서 5분 동안 고민하며 수정합니다.
- 업데이트 시도:
UPDATE Products SET price=20, version=2 WHERE id=1 AND version=1; - 확인: 수정된 행의 개수(Row Count)가 0인가요? 그렇다면, 내가 고민하는 사이에 누군가 버전을 2로 바꿔버린 것입니다.
- 에러 발생: "다른 사용자에 의해 데이터가 변경되었습니다."
- 사용 사례: 웹 애플리케이션, 긴 수정 양식.
5. 교착 상태 (Deadlock - 죽음의 순환)
두 트랜잭션이 서로가 쥐고 있는 락을 놓기를 기다리며 무한 대기 상태에 빠지는 현상입니다.
시나리오:
- 트랜잭션 A: 1번 행 락 보유 중. 2번 행을 원함.
- 트랜잭션 B: 2번 행 락 보유 중. 1번 행을 원함.
- 결과: 영원히 기다림.
DB의 대처:
데이터베이스에는 "Deadlock Detector(교착 상태 감지기)"라는 백그라운드 프로세스가 있습니다. 이것이 순환 고리를 감지하면, 둘 중 하나의 트랜잭션(희생자, Victim)을 강제로 종료(Kill) 시켜 다른 하나가 진행되도록 합니다.
6. 격리 수준 (Isolation Levels)
락은 트랜잭션 격리 수준을 구현하는 도구입니다. 수준이 엄격할수록 더 많은 락을 사용합니다.
- Read Uncommitted: 읽기 락(S-Lock)을 안 씁니다. 남이 아직 커밋하지 않은 데이터도 읽을 수 있습니다 ("Dirty Read"). 가장 빠르지만 위험함.
- Read Committed (Postgres/SQL Server 기본값): 커밋된 데이터만 읽습니다. 짧은 순간만 S-Lock을 겁니다.
- Repeatable Read (MySQL 기본값): 트랜잭션 내내 한 번 읽은 행은 값이 변하지 않음을 보장합니다. 더 강력한 락이나 스냅샷을 사용합니다.
- Serializable: 가장 엄격합니다. "Phantom Read"(없던 행이 갑자기 생기는 현상)를 막기 위해 범위 락(Range Lock)을 겁니다. 사실상 트랜잭션을 한 줄로 세워서 처리합니다. 가장 느림.
7. MVCC (다중 버전 동시성 제어)
프로 팁: 현대적인 데이터베이스(PostgreSQL, MySQL InnoDB, Oracle)는 읽을 때 단순한 S-Lock을 잘 쓰지 않습니다. 대신 MVCC를 사용합니다.
- 개념: 행을 업데이트할 때, DB는 기존 데이터를 덮어쓰지 않고 새로운 버전의 행을 만듭니다.
- 마법 같은 효과:
- 쓰는 사람(Writers) 은 새 버전을 잠급니다.
- 읽는 사람(Readers) 은 옛날 버전을 바라봅니다.
- 결과: 읽기 작업은 쓰기 작업을 막지 않고, 쓰기 작업도 읽기 작업을 막지 않습니다. 전통적인 락 방식보다 압도적인 성능을 제공합니다.
요약 체크리스트
- 읽기(Read) 는 보통 다른 읽기를 허용합니다 (공유).
- 쓰기(Write) 는 모든 것을 막습니다 (배타적).
- 교착 상태(Deadlock) 는 A가 B를 기다리고, B가 A를 기다릴 때 발생합니다.
- 웹 API 환경에서는 DB 락보다 낙관적 락(버전 컬럼 사용) 이 더 유리할 때가 많습니다.
references